package com.dashan.backend.geosearch;

import static java.lang.Math.PI;
import static java.lang.Math.acos;
import static java.lang.Math.atan;
import static java.lang.Math.cos;
import static java.lang.Math.log;
import static java.lang.Math.sin;
import static java.lang.Math.sinh;
import static java.lang.Math.sqrt;
import static java.lang.Math.tan;
import static java.lang.Math.toDegrees;
import static java.lang.Math.toRadians;

import com.dashan.backend.DashanException;
public class GeoMath {
    
    public static final double MAP_TOP = PI;
    public static final double MAP_BOTTOM = -PI;
    public static final double MAP_LEFT = -PI;
    public static final double MAP_RIGIT = PI;
    public static final double MAP_WIDTH = MAP_RIGIT - MAP_LEFT;
    public static final double MAP_HEIGHT = MAP_TOP - MAP_BOTTOM;
    
    public static final double EARTH_RADIUS = 6371004;
    
    public static final int MAX_LEVEL = 31;
    public static final int CELL_BITWIDTH = 62;
    
    public static final long[] MAGIC_NUM = {0x5555555555555555L, 0x3333333333333333L, 
                                            0x0f0f0f0f0f0f0f0fL, 0x00ff00ff00ff00ffL,
                                            0x0000ffff0000ffffL, 0x00000000ffffffffL};
    public static final long[] SHIFT_NUM = {1, 2, 4, 8, 16};

    
    /** @return map coordinate in radians */
    public static double lat2MapY(double lat) {
        lat = toRadians(lat);
        return log(tan(lat) + (1 / cos(lat)));
    }
    
    /** @return latitude in radians */
    public static double mapY2Lat(double mapInRadians) {
        return toDegrees(atan(sinh(mapInRadians)));
    }
    
    public static double lon2MapX(double lon) {
        lon = toRadians(lon);
        return lon;
    }
    
    public static double mapX2Lon(double mapInRadians) {
        return toDegrees(mapInRadians);
    }
    
    public static GeoCell encodeLatLonCoordinate(double lonInDegree, double latInDegree, 
            int level) throws DashanException {
        return encodeMapCoordinate(lon2MapX(toRadians(lonInDegree)),
                                   lat2MapY(toRadians(latInDegree)), 
                                   level);
    }
    public static GeoCell encodeMapCoordinate(double mapX, double mapY, int level) 
        throws DashanException {
        if (level < 0 || level > MAX_LEVEL) {
            throw new DashanException("Level must between 0 and 32");
        }
        return new GeoCell(level, (int) mapX2IndexX(level, mapX), (int) mapY2IndexY(level, mapY));
    }
    
    public static long mapX2IndexX(int level, double mapX) {
        return (long) (((mapX - MAP_LEFT) / MAP_WIDTH) * (0x1L << (level)));
    }
    
    public static long mapY2IndexY(int level, double mapY) {
        return (long) (((MAP_TOP - mapY) / MAP_HEIGHT) * (0x1L << (level)));
    }
    
    public static double indexX2MapX(int level, long indexX) {
        return (indexX * MAP_WIDTH) / (0x1L << level) + MAP_LEFT;
    }
    
    public static double indexY2MapY(int level, long indexY) {
        return MAP_TOP - (indexY * MAP_HEIGHT) / (0x1L << level);
    }
    
    public static double getMapDistance(double mapX1, double mapY1, double mapX2, double mapY2) {
        return sqrt((mapX1 - mapX2) *(mapX1 - mapX2) + (mapY1 - mapY2) * (mapY1 - mapY2)); 
    }
    
    public static double getMapDistanceByLonLat(double lon1, double lat1, double lon2, double lat2) {
        return getMapDistance(lon2MapX(lon1), lat2MapY(lat1), lon2MapX(lon2), lat2MapY(lat2));
    }

    public static double getDistanceByLonLat(double lon1, double lat1, double lon2, double lat2) {
        double a = toRadians(lon1);
        double b = toRadians(lat1);
        double x = toRadians(lon2);
        double y = toRadians(lat2);
        
        return EARTH_RADIUS * acos(cos(b) * cos(y) * cos(a - x) + sin(b) * sin(y));
    }
    
    public static double getLatSpanByDistance(double distance) {
        return toDegrees(distance / EARTH_RADIUS);
    }
    
    public static double getLonSpanByDistance(double distance, double lat) {
        return toDegrees(distance) / (EARTH_RADIUS * cos(toRadians(lat)));
    }
    
    public static long interleaveLong(long x, long y) {
        x = (x | (x << SHIFT_NUM[4])) & MAGIC_NUM[4];
        x = (x | (x << SHIFT_NUM[3])) & MAGIC_NUM[3];
        x = (x | (x << SHIFT_NUM[2])) & MAGIC_NUM[2];
        x = (x | (x << SHIFT_NUM[1])) & MAGIC_NUM[1];
        x = (x | (x << SHIFT_NUM[0])) & MAGIC_NUM[0];

        y = (y | (y << SHIFT_NUM[4])) & MAGIC_NUM[4];
        y = (y | (y << SHIFT_NUM[3])) & MAGIC_NUM[3];
        y = (y | (y << SHIFT_NUM[2])) & MAGIC_NUM[2];
        y = (y | (y << SHIFT_NUM[1])) & MAGIC_NUM[1];
        y = (y | (y << SHIFT_NUM[0])) & MAGIC_NUM[0];
        
        return x | y << 1;
    }
    
    /** Extract x/y from a mortonNumber. */
    public static long[] extractLong(long mortonNumber) {
        long x = mortonNumber & MAGIC_NUM[0];
        x = (x | x >> SHIFT_NUM[0]) & MAGIC_NUM[1];
        x = (x | x >> SHIFT_NUM[1]) & MAGIC_NUM[2];
        x = (x | x >> SHIFT_NUM[2]) & MAGIC_NUM[3];
        x = (x | x >> SHIFT_NUM[3]) & MAGIC_NUM[4];
        x = (x | x >> SHIFT_NUM[4]) & MAGIC_NUM[5];
        
        long y = (mortonNumber >> 1) & MAGIC_NUM[0];
        y = (y | y >> SHIFT_NUM[0]) & MAGIC_NUM[1];
        y = (y | y >> SHIFT_NUM[1]) & MAGIC_NUM[2];
        y = (y | y >> SHIFT_NUM[2]) & MAGIC_NUM[3];
        y = (y | y >> SHIFT_NUM[3]) & MAGIC_NUM[4];
        y = (y | y >> SHIFT_NUM[4]) & MAGIC_NUM[5];
     
        long [] res = {x, y};
        return res;
    }
}
